/*
 * SubsetXPathCompiler.java
 *
 * Created on January 21, 2003, 8:35 PM
 */

package com.smallx.xpath.streaming;

import com.smallx.xpath.StaticContext;
import com.smallx.xpath.XPathCompiler;
import com.smallx.xpath.XPathExpression;
import com.smallx.xpath.XPathMatcherFactory;
import com.smallx.xpath.parser.Expression;
import com.smallx.xpath.parser.XPathParser;
import java.util.*;
import org.infoset.xml.Attribute;
import org.infoset.xml.Characters;
import org.infoset.xml.Child;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Item;
import org.infoset.xml.Name;
import org.infoset.xml.Parent;
import org.infoset.xml.XMLException;

/**
 *
 * @author  alex
 */
public class SubsetXPathCompiler implements XPathCompiler {
   
   static class SingleNodeIterator implements Iterator<Item> {
      
      Item node;
      SingleNodeIterator(Item node) {
         this.node = node;
      }
      
      public void remove() {}
      
      public boolean hasNext() {
         return node!=null;
      }
      
      public Item next() {
         Item toReturn = null;
         if (node!=null) {
            toReturn = node;
            node = null;
         }
         return toReturn;
      }
   }
   
   interface StepExpression extends XPathExpression {
      Iterator<? extends Item> eval(Iterator<Item> context);
   }
   
   static abstract class ExpressionBase implements XPathExpression {
      ExpressionBase() {
      }
      
      abstract public Iterator<? extends Item> eval(Item context) throws XMLException;
      
      /**
       * Applies the expression and returns the first matching node of the result.
       * @param context The context node for the expresion.
       */
      public Item selectFirst(Item context) 
         throws XMLException
      {
         Iterator<? extends Item> results = eval(context);
         return results.hasNext() ? results.next() : null;
      }
      
      /**
       * Applies the expression and returns the string value of the result.
       * @param context The context node for the expresion.
       */
      public String valueOf(Item context) 
         throws XMLException
      {
         Iterator<? extends Item> results = eval(context);
         StringBuffer buffer = new StringBuffer();
         while (results.hasNext()) {
            Item node = results.next();
            switch (node.getType()) {
               case ElementItem:
               case DocumentItem:
                  valueOf(buffer,((Parent)node).iterator());
                  break;
               case AttributeItem:
                  buffer.append(((Attribute)node).getText());
                  break;
               case CharactersItem:
                  buffer.append(((Characters)node).getText());
                  break;
            }
         }
         return buffer.toString();
      }
      
      void valueOf(StringBuffer buffer,Iterator<? extends Item> nodes) {
         while (nodes.hasNext()) {
            Item node = (Item)nodes.next();
            switch (node.getType()) {
               case ElementItem:
               case DocumentItem:
                  valueOf(buffer,((Parent)node).iterator());
                  break;
               case CharactersItem:
                  buffer.append(((Characters)node).getText());
                  break;
            }
         }
      }
      
   }

   static class OrExpression extends ExpressionBase {
      List<XPathExpression> expressions;
      OrExpression() {
         expressions = new ArrayList<XPathExpression>();
      }
      
      void addExpression(XPathExpression expr) {
         expressions.add(expr);
      }
      
      public Iterator<Item> eval(Item context) throws XMLException
      {
         List results = new ArrayList();
         Iterator<XPathExpression> exprs = expressions.iterator();
         while (exprs.hasNext()) {
            XPathExpression expr = exprs.next();
            Iterator<? extends Item> values = expr.eval(context);
            while (values.hasNext()) {
               Item item = values.next();
               Iterator beforeResults = results.iterator();
               int index = 0;
               while(beforeResults.hasNext()) {
                  Item resultItem = (Item)beforeResults.next();
                  if (resultItem.compareTo(item)>=0) {
                     break;
                  }
                  index++;
               }
               results.add(index,item);
            }
         }
         return results.iterator();
      }
      
   }
   
   static class RootExpression extends ExpressionBase {
      
      XPathExpression subexpr;
      RootExpression(XPathExpression subexpr) {
         this.subexpr = subexpr;
      }
      
      public Iterator<? extends Item> eval(Item context) 
         throws XMLException
      {
         Document doc = context instanceof Parent ? ((Parent)context).getDocument() : context.getOrigin().getDocument();
         return subexpr==null ? new SingleNodeIterator(doc) : subexpr.eval(doc);
      }
      
   }
   
   static class AttributeStepExpression extends ExpressionBase implements StepExpression {
      Name name;
      AttributeStepExpression(Name name) {
         this.name = name;
      }
      
      public Iterator<? extends Item> eval(Item context) {
         Attribute att = null;
         if (context.getType()==Item.ItemType.ElementItem) {
            if (name==null) {
               return ((Element)context).getAttributes().values().iterator();
            } else {
               att = ((Element)context).getAttributes().get(name);
            }
         }
         return new SingleNodeIterator(att);
      }
      
      public Iterator<? extends Item> eval(Iterator contextList) {
         List matched = new ArrayList();
         while (contextList.hasNext()) {
            Item context = (Item)contextList.next();
            if (context.getType()==Item.ItemType.ElementItem) {
               if (name==null) {
                  for (Attribute att : ((Element)context).getAttributes().values()) {
                     matched.add(att);
                  }
               } else {
                  Attribute att = ((Element)context).getAttributes().get(name);
                  if (att!=null) {
                     matched.add(att);
                  }
               }
            }
         }
         return matched.iterator();
      }
      
   }
   
   static class ElementStepExpression extends ExpressionBase implements StepExpression {
      
      Name name;
      StepExpression subexpr;
      
      ElementStepExpression(Name name,StepExpression subexpr) {
         this.name = name;
         this.subexpr = subexpr;
      }
      
      void eval(List matched,Item context) {
         if (context instanceof Parent) {
            for (Child child : ((Parent)context)) {
               if (child.getType()==Item.ItemType.ElementItem && (name==null || ((Element)child).getName().equals(name))) {
                  matched.add(child);
               }
            }
         }
      }
      
      public Iterator<? extends Item> eval(Item context) {
         List<Item> matched = new ArrayList();
         eval(matched,context);
         if (matched.size()==0) {
            return new SingleNodeIterator(null);
         } else {
            return subexpr==null ? matched.iterator() : subexpr.eval(matched.iterator());
         }
      }
      
      public Iterator eval(Iterator contextList) {
         List<Item> matched = new ArrayList();
         while (contextList.hasNext()) {
            Item context = (Item)contextList.next();
            eval(matched,context);
         }
         if (matched.size()==0) {
            return new SingleNodeIterator(null);
         } else {
            return subexpr==null ? matched.iterator() : subexpr.eval(matched.iterator());
         }
      }
      
   }

   static class TextStepExpression extends ExpressionBase implements StepExpression {
      
      TextStepExpression() {
      }
      
      void eval(List<Item> matched,Item context) {
         if (context instanceof Parent) {
            for (Child child : ((Parent)context)) {
               if (child.getType()==Item.ItemType.CharactersItem) {
                  matched.add(child);
               }
            }
         }
      }
      
      public Iterator<? extends Item> eval(Item context) {
         List matched = new ArrayList();
         eval(matched,context);
         if (matched.size()==0) {
            return new SingleNodeIterator(null);
         } else {
            return matched.iterator();
         }
      }
      
      public Iterator<? extends Item> eval(Iterator<Item> contextList) {
         List<Item> matched = new ArrayList();
         while (contextList.hasNext()) {
            Item context = contextList.next();
            eval(matched,context);
         }
         if (matched.size()==0) {
            return new SingleNodeIterator(null);
         } else {
            return matched.iterator();
         }
      }
      
   }

   static class DocumentCompiler {
      static XPathExpression compile(StaticContext context,String expression) 
         throws XMLException 
      {
         expression = expression.trim();
         int or = expression.indexOf('|');
         if (or>=0) {
            OrExpression orexpr = new OrExpression();
            String subexpr = expression.substring(0,or);
            do {
               orexpr.addExpression(compile(context,subexpr));
               if (or>=0) {
                  expression = expression.substring(or+1);
                  or = expression.indexOf('|');
                  subexpr = or<0 ? expression : expression.substring(0,or);
               } else {
                  subexpr = null;
               }
            } while (subexpr!=null);
            return orexpr;
         } else if (expression.charAt(0)=='/') {
            return new RootExpression(parseExpression(context,expression.substring(1)));
         } else {
            return parseExpression(context,expression);
         }
      }

      private static XPathExpression parseExpression(StaticContext context,String expr) 
         throws XMLException
      {
         if (expr.length()==0) {
            return null;
         }
         if (expr.charAt(0)=='@') {
            return parseAttributeStep(context,expr);
         } else {
            return parseElementOrFunctionStep(context,expr);
         }
      }

      private static StepExpression parseAttributeStep(StaticContext context,String expr) 
         throws XMLException
      {
         if (expr.equals("@*")) {
            return new AttributeStepExpression(null);
         } else {
            return new AttributeStepExpression(context.getNamespaceScope().expandName(expr.substring(1),false));
         }
      }

      private static StepExpression parseStep(StaticContext context,String expr) 
         throws XMLException
      {
         if (expr.length()==0) {
            return null;
         }
         if (expr.charAt(0)=='@') {
            return parseAttributeStep(context,expr);
         } else {
            return parseElementOrFunctionStep(context,expr);
         }
      }

      private static StepExpression parseElementOrFunctionStep(StaticContext context,String expr) 
         throws XMLException
      {
         int len = expr.length();
         int end = 0;
         for (; end<len; end++) {
            char ch = expr.charAt(end);
            if (ch=='(' || ch=='/') {
               break;
            }
         }
         if (end==len) { // At the end
            return parseElementStep(context,expr,null);
         } else if (expr.charAt(end)=='/') { // trails with a slash
            return parseElementStep(context,expr.substring(0,end),expr.substring(end+1));
         } else { // A function call
            // text() is all that is supported
            if ((end+1)<len && expr.charAt(end+1)==')' && expr.startsWith("text(")) {
               if (len>(end+2)) {
                  throw new XMLException("Step '"+expr.substring(end+2)+"' following text() function not allowed in subset");
               } 
               return new TextStepExpression();
            } else {
               throw new XMLException("Unknown function call "+expr.substring(0,end));
            }
         }
      }

      private static StepExpression parseElementStep(StaticContext context,String qname,String subexpr) 
         throws XMLException
      {
         if (subexpr==null || subexpr.length()==0) {
            if (qname.equals("*")) {
               return new ElementStepExpression(null,null);
            } else {
               return new ElementStepExpression(context.getNamespaceScope().expandName(qname,false),null);
            }
         } else {
            if (qname.equals("*")) {
               return new ElementStepExpression(null,parseStep(context,subexpr));
            } else {
               return new ElementStepExpression(context.getNamespaceScope().expandName(qname,false),parseStep(context,subexpr));
            }
         }
      }
   }
   
   StaticContext context;
   
   /** Creates a new instance of SubsetXPathCompiler */
   public SubsetXPathCompiler(StaticContext context) {
      this.context = context;
   }
   
   public XPathExpression compile(String expression) throws XMLException {
      XPathParser parser = new XPathParser(context);
      parser.setExpression(expression);
      Expression e = parser.parseExpr();
      throw new XMLException("Not implemented.");
   }
   
   public XPathExpression compile(StaticContext localContext,String expression) throws XMLException {
      XPathParser parser = new XPathParser(localContext);
      parser.setExpression(expression);
      Expression e = parser.parseExpr();
      throw new XMLException("Not implemented.");
   }
   
   /** Compiles an XPath expression.
    * @param context The static context against which this expression will be compiled.
    * @param expression The XPath expression to compile.
    * @throws XMLException Thrown when there is a static error compiling the XPath
    */
   public XPathMatcherFactory compileMatcher(String expression) throws XMLException {
      XPathParser parser = new XPathParser(context);
      parser.setExpression(expression);
      Expression e = parser.parseExpr();
      return compileMatcher(e);
   }
   
   public XPathMatcherFactory compileMatcher(StaticContext localContext,String expression) throws XMLException {
      XPathParser parser = new XPathParser(localContext);
      parser.setExpression(expression);
      Expression e = parser.parseExpr();
      return compileMatcher(e);
   }
   
   public StaticContext getStaticContext() {
      return context;
   }
   
   public void setStaticContext(StaticContext context) {
      this.context = context;
   }
   
   protected XPathMatcherFactory compileMatcher(Expression e) 
      throws XMLException
   {
      return SubsetMatchCompiler.compile(e);
   }
   
}
